﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Net;

namespace Heimdallr.Net
{
    /// <summary>
    /// Represents the module in an <see cref="HttpElement"/> instance.
    /// </summary>
    /// <remarks>
    /// Adding modules to <see cref="HttpElement.Modules"/> property for handling incoming request and outgoing response. 
    /// Each module corresponds to a combination of URL path and HTTP method.
    /// For handling requests, a module must implement <see cref="IHttpRequestModule"/> interface.
    /// For handling responses, implements <see cref="IHttpResponseModule"/> interface.
    /// </remarks>
    public abstract class HttpModule : IDisposable
    {
        #region Field

        /// <summary>
        /// Gets the empty buffer.
        /// This field is read-only.
        /// </summary>
        public readonly static byte[] EmptyBuffer = new byte[0];

        private readonly UriBuilder uri;
        private readonly string method;

        private HttpModuleCredentialType credentialType;

        #endregion

        #region Property

        /// <summary>
        /// Gets the HTTP method.
        /// </summary>
        public string Method
        {
            get { return this.method; }
        }

        /// <summary>
        /// Gets the relative path.
        /// </summary>
        public string RelativePath
        {
            get { return this.uri.Path; }
        }

        /// <summary>
        /// Gets the credential type.
        /// </summary>
        public HttpModuleCredentialType CredentialType
        {
            get { return this.credentialType; }
            protected set { this.credentialType = value; }
        }

        #endregion

        #region Constructor

        /// <summary>
        /// Initializes a new instance with indicated HTTP method, path and anonymous authentication.
        /// </summary>
        /// <param name="method">HTTP method.</param>
        /// <param name="relativePath">HTTP relative path.</param>
        /// <exception cref="ArgumentNullException">Parameter <paramref name="method"/> or <paramref name="relativePath"/> is null.</exception>
        /// <exception cref="ArgumentException">Parameter <paramref name="relativePath"/> must end with character '/'.</exception>
        protected HttpModule(string method, string relativePath)
            : this(method, relativePath, HttpModuleCredentialType.None)
        {
        }

        /// <summary>
        /// Initializes a new instance with indicated HTTP method, path and credential type.
        /// </summary>
        /// <param name="method">HTTP method.</param>
        /// <param name="relativePath">HTTP relative path.</param>
        /// <param name="credentialType">HTTP credential type.</param>
        /// <exception cref="ArgumentNullException">Parameter <paramref name="method"/> or <paramref name="relativePath"/> is null.</exception>
        /// <exception cref="ArgumentException">Parameter <paramref name="relativePath"/> must end with character '/'.</exception>
        /// <exception cref="InvalidEnumArgumentException">Parameter <paramref name="credentialType"/> is not a valid value.</exception>
        protected HttpModule(string method, string relativePath, HttpModuleCredentialType credentialType)
        {
            if (method == null)
                throw new ArgumentNullException("method");

            if (relativePath == null)
                throw new ArgumentNullException("relativePath");

            if (relativePath.LastOrDefault() != '/')
                throw new ArgumentException("Must end with character '/'.", "relativePath");

            if (!Enum.IsDefined(typeof(HttpModuleCredentialType), credentialType))
                throw new InvalidEnumArgumentException("credentialType", (int)credentialType, typeof(HttpModuleCredentialType));

            this.method = method.Trim().ToUpperInvariant();
            this.uri = new UriBuilder(Uri.UriSchemeHttp, Dns.GetHostName(), 80, relativePath.ToLowerInvariant());
            this.credentialType = credentialType;
        }

        #endregion

        #region Method

        /// <summary>
        /// When implemented in a derived class, indicates if user name and password of incoming request is valid or not.
        /// </summary>
        /// <param name="username">User name of incoming request.</param>
        /// <param name="password">Password of incoming request.</param>
        /// <returns>If valid, the return value is true. Otherwise false.</returns>
        public abstract bool Validate(string username, string password);

        /// <summary>
        /// When implemented in a derived class, performs tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public abstract void Dispose();

        #endregion
    }

    /// <summary>
    /// Specifies protocols for HTTP credential types.
    /// </summary>
    public enum HttpModuleCredentialType
    {
        /// <summary>
        /// Specifies anonymous authentication.
        /// </summary>
        None,

        /// <summary>
        /// Specifies basic authentication.
        /// </summary>
        /// <remarks>
        /// For additional information, see RFC2617 – HTTP Authentication: Basic and Digest Authentication. 
        /// </remarks>
        Basic,

        /// <summary>
        /// Specifies digest authentication.
        /// </summary>
        /// <remarks>
        /// For additional information, see RFC2617 – HTTP Authentication: Basic and Digest Authentication. 
        /// </remarks>
        Digest,

        /// <summary>
        /// Specifies NTLM authentication.
        /// </summary>
        Ntlm,

        /// <summary>
        /// Specifies Windows authentication.
        /// </summary>
        Windows,
    }

    class HttpModuleCollection : Collection<HttpModule>
    {
        #region Field

        private readonly ConcurrentDictionary<string, IEnumerable<IHttpRequestModule>> requestMapping;
        private readonly ConcurrentDictionary<string, IEnumerable<IHttpResponseModule>> responseMapping;

        #endregion

        #region Constructor

        public HttpModuleCollection()
        {
            this.requestMapping = new ConcurrentDictionary<string, IEnumerable<IHttpRequestModule>>();
            this.responseMapping = new ConcurrentDictionary<string, IEnumerable<IHttpResponseModule>>();
        }

        #endregion

        #region Public Method

        public IEnumerable<IHttpRequestModule> GetRequestModules(string relativePath)
        {
            return this.requestMapping.GetOrAdd(relativePath, this.FindRequestModules);
        }

        public IEnumerable<IHttpResponseModule> GetResponseModules(string relativePath)
        {
            return this.responseMapping.GetOrAdd(relativePath, this.FindResponseModules);
        }

        public IEnumerable<string> GetAllowedHttpMethods(string relativePath)
        {
            if (relativePath == null)
                throw new ArgumentNullException("relativePath");

            return from module in base.Items
                   from pathAndMethod in PathAndMethod.FromModule(module)
                   where pathAndMethod.RelativePath.StartsWith(relativePath)
                   select pathAndMethod.Method;
        }

        #endregion

        #region Overridden Method

        protected override void InsertItem(int index, HttpModule item)
        {
            if (item == null)
                throw new ArgumentNullException("item");

            base.InsertItem(index, item);

            if (item is IHttpRequestModule)
                this.requestMapping.Clear();

            if (item is IHttpResponseModule)
                this.responseMapping.Clear();
        }

        protected override void SetItem(int index, HttpModule item)
        {
            if (item == null)
                throw new ArgumentNullException("item");

            base.SetItem(index, item);

            if (item is IHttpRequestModule)
                this.requestMapping.Clear();

            if (item is IHttpResponseModule)
                this.responseMapping.Clear();
        }

        protected override void RemoveItem(int index)
        {
            HttpModule item = base.Items[index];

            base.RemoveItem(index);

            if (item is IHttpRequestModule)
                this.requestMapping.Clear();

            if (item is IHttpResponseModule)
                this.responseMapping.Clear();
        }

        protected override void ClearItems()
        {
            base.ClearItems();

            this.requestMapping.Clear();
            this.responseMapping.Clear();
        }

        #endregion

        #region Misc

        private IEnumerable<IHttpRequestModule> FindRequestModules(string relativePath)
        {
            return from module in FindModules<IHttpRequestModule, IHttpRequestModule>(base.Items)
                   where module.RelativePath.StartsWith(relativePath, StringComparison.InvariantCultureIgnoreCase)
                   select module;
        }

        private IEnumerable<IHttpResponseModule> FindResponseModules(string relativePath)
        {
            return from module in FindModules<IHttpResponseModule, IHttpResponseModule>(base.Items)
                   where module.RelativePath.StartsWith(relativePath, StringComparison.InvariantCultureIgnoreCase)
                   select module;
        }

        public static IEnumerable<TResult> FindModules<TTarget, TResult>(IEnumerable<HttpModule> modules)
            where TTarget : class
            where TResult : class
        {
            return from module in modules
                   where module is TTarget && module is TResult
                   select module as TResult;
        }

        #endregion
    }

    struct PathAndMethod : IEquatable<PathAndMethod>, IComparable<PathAndMethod>
    {
        #region Field

        private string path;
        private string method;

        #endregion

        #region Property

        public string RelativePath
        {
            get { return this.path; }
        }

        public string Method
        {
            get { return this.method; }
        }

        #endregion

        #region Constructor

        public PathAndMethod(string path, string method)
        {
            this.path = path.ToLowerInvariant();
            this.method = method;
        }

        #endregion

        #region Override

        public override bool Equals(object obj)
        {
            if (obj is PathAndMethod)
                return this.Equals((PathAndMethod)obj);
            else
                return false;
        }

        public override int GetHashCode()
        {
            return this.path.GetHashCode() ^ this.method.GetHashCode();
        }

        public override string ToString()
        {
            return string.Format("{0}, {1}", this.path, this.method);
        }

        #endregion

        #region Static Factory

        public static PathAndMethod FromModule(IHttpRequestModule module)
        {
            return new PathAndMethod(module.RelativePath, module.Method);
        }

        public static PathAndMethod FromModule(IHttpResponseModule module)
        {
            return new PathAndMethod(module.RelativePath.ToLowerInvariant(), module.Method);
        }

        public static PathAndMethod[] FromModule(HttpModule module)
        {
            if (module is IHttpRequestModule && module is IHttpResponseModule)
                return new[] 
                    {
                        FromModule(module as IHttpRequestModule), 
                        FromModule(module as IHttpResponseModule) 
                    };

            if (module is IHttpRequestModule)
                return new[] { FromModule(module as IHttpRequestModule) };

            if (module is IHttpResponseModule)
                return new[] { FromModule(module as IHttpResponseModule) };

            return new PathAndMethod[0];
        }

        #endregion

        #region IEquatable Member

        public bool Equals(PathAndMethod other)
        {
            return this.path == other.path && this.method == other.method;
        }

        #endregion

        #region IComparable Member

        public int CompareTo(PathAndMethod other)
        {
            if (this.path != other.path)
                return this.path.CompareTo(other.path);
            else
                return this.method.CompareTo(other.method);
        }

        #endregion

        #region Misc

        public static string GetRelativePath(Uri requestUri)
        {
            string absolutePath = requestUri.AbsolutePath.TrimStart('/');
            string relativePath;

            int indexOfLastSlide = absolutePath.LastIndexOf('/');

            if (indexOfLastSlide >= 0)
                relativePath = absolutePath.Substring(0, indexOfLastSlide + 1);
            else
                relativePath = absolutePath;

            return relativePath;
        }

        #endregion
    }
}